TI_GDT equ  0
RPL0  equ   0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

section .data
put_int_buffer    dq    0     ; 定义8字节缓冲区用于数字到字符的转换

[bits 32]
section .text
    ;--------------------------------------------
    ;put_str 通过put_char来打印以0字符结尾的字符串
    ;--------------------------------------------
global put_str
put_str:
    ;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
    push ebx
    push ecx
    xor ecx, ecx		      ; 准备用ecx存储参数,清空
    mov ebx, [esp + 12]	      ; 从栈中得到待打印的字符串地址 
.goon:
    mov cl, [ebx]
    cmp cl, 0		      ; 如果处理到了字符串尾,跳到结束处返回
    jz .str_over
    push ecx		      ; 为put_char函数传递参数
    call put_char
    add esp, 4		      ; 回收参数所占的栈空间
    inc ebx		      ; 使ebx指向下一个字符
    jmp .goon
.str_over:
    pop ecx
    pop ebx
    ret


    ;--------------------   将小端字节序的数字变成对应的ascii后，倒置   -----------------------
    ;输入：栈中参数为待打印的数字
    ;输出：在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时，只会直接打印f，不会是0xf
    ;------------------------------------------------------------------------------------------
global put_int
put_int:
    pushad
    mov ebp, esp
    mov eax, [ebp+4*9]		       ; call的返回地址占4字节+pushad的8个4字节
    mov edx, eax
    mov edi, 7                          ; 指定在put_int_buffer中初始的偏移量
    mov ecx, 8			       ; 32位数字中,16进制数字的位数是8个
    mov ebx, put_int_buffer

    ;将32位数字按照16进制的形式从低位到高位逐个处理,共处理8个16进制数字
.16based_4bits:			       ; 每4位二进制是16进制数字的1位,遍历每一位16进制数字
    and edx, 0x0000000F		       ; 解析16进制数字的每一位。and与操作后,edx只有低4位有效
    cmp edx, 9			       ; 数字0～9和a~f需要分别处理成对应的字符
    jg .is_A2F 
    add edx, '0'			       ; ascii码是8位大小。add求和操作后,edx低8位有效。
    jmp .store
.is_A2F:
    sub edx, 10			       ; A~F 减去10 所得到的差,再加上字符A的ascii码,便是A~F对应的ascii码
    add edx, 'A'

    ;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer
    ;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.
.store:
    ; 此时dl中应该是数字对应的字符的ascii码
    mov [ebx+edi], dl		       
    dec edi
    shr eax, 4
    mov edx, eax 
    loop .16based_4bits

    ;现在put_int_buffer中已全是字符,打印之前,
    ;把高位连续的字符去掉,比如把字符000123变成123
.ready_to_print:
    inc edi			       ; 此时edi退减为-1(0xffffffff),加1使其为0
.skip_prefix_0:  
    cmp edi,8			       ; 若已经比较第9个字符了，表示待打印的字符串为全0 
    je .full0 
    ;找出连续的0字符, edi做为非0的最高位字符的偏移
.go_on_skip:   
    mov cl, [put_int_buffer+edi]
    inc edi
    cmp cl, '0' 
    je .skip_prefix_0		       ; 继续判断下一位字符是否为字符0(不是数字0)
    dec edi			       ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符		       
    jmp .put_each_num

.full0:
    mov cl,'0'			       ; 输入的数字为全0时，则只打印0
.put_each_num:
    push ecx			       ; 此时cl中为可打印的字符
    call put_char
    add esp, 4
    inc edi			       ; 使edi指向下一个字符
    mov cl, [put_int_buffer+edi]	       ; 获取下一个字符到cl寄存器
    cmp edi,8
    jl .put_each_num
    popad
    ret

    ;------------------------   put_char   -----------------------------
    ;功能描述:把栈中的1个字符写入光标所在处
    ;-------------------------------------------------------------------   
global put_char
put_char:
    pushad	   ;备份32位寄存器环境
    ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
    mov ax, SELECTOR_VIDEO	       ; 不能直接把立即数送入段寄存器
    mov gs, ax

    ;;;;;;;;;  获取当前光标位置 ;;;;;;;;;
    ;先获得高8位
    mov dx, 0x03d4  ;索引寄存器
    mov al, 0x0e	   ;用于提供光标位置的高8位
    out dx, al
    mov dx, 0x03d5  ;通过读写数据端口0x3d5来获得或设置光标位置 
    in al, dx	   ;得到了光标位置的高8位
    mov ah, al

    ;再获取低8位
    mov dx, 0x03d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x03d5 
    in al, dx

    ;将光标存入bx
    mov bx, ax	  
    ;下面这行是在栈中获取待打印的字符
    mov ecx, [esp + 36]	      ;pushad压入4×8＝32字节,加上主调函数的返回地址4字节,故esp+36字节
    cmp cl, 0xd				  ;CR是0x0d,LF是0x0a
    jz .is_carriage_return
    cmp cl, 0xa
    jz .is_line_feed

    cmp cl, 0x8				  ;BS(backspace)的asc码是8
    jz .is_backspace
    jmp .put_other	   
    ;;;;;;;;;;;;;;;;;;

.is_backspace:		      
    ;;;;;;;;;;;;       backspace的一点说明	     ;;;;;;;;;;
    ; 当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
    ; 但有可能在键入backspace后并不再键入新的字符,这时在光标已经向前移动到待删除的字符位置,但字符还在原处,
    ; 这就显得好怪异,所以此处添加了空格或空字符0
    dec bx
    shl bx,1
    mov byte [gs:bx], 0x20		  ;将待删除的字节补为0或空格皆可
    inc bx
    mov byte [gs:bx], 0x07
    shr bx,1
    jmp .set_cursor
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

.put_other:
    shl bx, 1				  ; 光标位置是用2字节表示,将光标值乘2,表示对应显存中的偏移字节
    mov [gs:bx], cl			  ; ascii字符本身
    inc bx
    mov byte [gs:bx],0x07		  ; 字符属性
    shr bx, 1				  ; 恢复老的光标值
    inc bx				  ; 下一个光标值
    cmp bx, 2000		   
    jl .set_cursor			  ; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
                        ; 若超出屏幕字符数大小(2000)则换行处理
.is_line_feed:				  ; 是换行符LF(\n)
.is_carriage_return:			  ; 是回车符CR(\r)
                        ; 如果是CR(\r),只要把光标移到行首就行了。
    xor dx, dx				  ; dx是被除数的高16位,清0.
    mov ax, bx				  ; ax是被除数的低16位.
    mov si, 80				  ; 由于是效仿linux，linux中\n便表示下一行的行首，所以本系统中，
    div si				  ; 把\n和\r都处理为linux中\n的意思，也就是下一行的行首。
    sub bx, dx				  ; 光标值减去除80的余数便是取整
                        ; 以上4行处理\r的代码

.is_carriage_return_end:                 ; 回车符CR处理结束
    add bx, 80
    cmp bx, 2000
    .is_line_feed_end:			  ; 若是LF(\n),将光标移+80便可。  
    jl .set_cursor

    ;屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
.roll_screen:				  ; 若超出屏幕大小，开始滚屏
    cld  
    mov ecx, 960				  ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次 
    mov esi, 0xc00b80a0			  ; 第1行行首
    mov edi, 0xc00b8000			  ; 第0行行首
    rep movsd				  

    ;;;;;;;将最后一行填充为空白
    mov ebx, 3840			  ; 最后一行首字符的第一个字节偏移= 1920 * 2
    mov ecx, 80				  ;一行是80字符(160字节),每次清理1字符(2字节),一行需要移动80次
.cls:
    mov word [gs:ebx], 0x0720		  ;0x0720是黑底白字的空格键
    add ebx, 2
    loop .cls 
    mov bx,1920				  ;将光标值重置为1920,最后一行的首字符.

.set_cursor:   
                        ;将光标设为bx值
    ;;;;;;; 1 先设置高8位 ;;;;;;;;
    mov dx, 0x03d4			  ;索引寄存器
    mov al, 0x0e				  ;用于提供光标位置的高8位
    out dx, al
    mov dx, 0x03d5			  ;通过读写数据端口0x3d5来获得或设置光标位置 
    mov al, bh
    out dx, al

    ;;;;;;; 2 再设置低8位 ;;;;;;;;;
    mov dx, 0x03d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x03d5 
    mov al, bl
    out dx, al
    .put_char_done: 
    popad
    ret


